﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Runtime.InteropServices;

namespace MonoStrategy
{
    internal enum GameMapCommand : byte
    {
        DropResource = 0,
        AddBuilding = 1,
        AddMovable = 2,
        Idle = 3,
        ChangeProfession = 4,
        ChangeDistributionSetting = 5,
        ChangeToolSetting = 6,
        AddMarketTransport = 7,
        RemoveMarketTransport = 8,
        SetBuildingSuspended = 9,
        LowerBuildingPriority = 10,
        RaiseBuildingPriority = 11,
        RemoveBuilding = 12,
        RaiseTaskPriority = 13,
        RemoveBuildTask = 14,
        SetTaskSuspended = 15,
        SetWorkingArea = 16,
    }

    internal class GameMapFile : IDisposable
    {
        internal static GameMapFile OpenWrite(String inFileName, GameMapFile inDeriveFrom)
        {
            return new GameMapFile(inFileName, false);
        }

        internal static GameMapFile OpenRead(String inFileName)
        {
            return new GameMapFile(inFileName, true);
        }

        private FileStream m_Stream;
        private BinaryReader m_Reader;
        private BinaryWriter m_Writer;
        private bool m_IsReadMode;
        private readonly Object m_Lock = new Object();
        internal Int64 NextCycle { get; private set; }

        internal String FileName { get; private set; }

        public void Dispose()
        {
            if (m_Stream != null)
                m_Stream.Dispose();

            NextCycle = -1;
            FileName = null;
            m_Stream = null;
            m_Reader = null;
            m_Writer = null;
        }

        private GameMapFile(String inFileName, bool inIsReadMode)
        {
            m_IsReadMode = inIsReadMode;
            FileName = Path.GetFullPath(inFileName);

            if (m_IsReadMode)
            {
                m_Stream = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.Read, 1024 * 1024, FileOptions.SequentialScan);
                m_Reader = new BinaryReader(m_Stream);

                if (m_Stream.Length > 0)
                    NextCycle = m_Reader.ReadInt64();
                else
                    NextCycle = -1;
            }
            else
            {
                m_Stream = new FileStream(FileName, FileMode.Create, FileAccess.Write, FileShare.Read, 1024 * 1024, FileOptions.WriteThrough);
                m_Writer = new BinaryWriter(m_Stream);
            }
        }

        private void ForceWriteMode()
        {
            if (m_IsReadMode)
                throw new InvalidOperationException("Map file is not in write mode!");
        }

        internal void AddBuilding(Int64 inCycleTime, BuildingConfig inConfig, Point inPosition)
        {
            lock (m_Lock)
            {
                ForceWriteMode();

                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.AddBuilding);
                m_Writer.Write((Int16)inConfig.TypeIndex);
                m_Writer.Write((Int16)inPosition.X);
                m_Writer.Write((Int16)inPosition.Y);
                m_Writer.Flush();
            }
        }

        internal void DropResource(Int64 inCycleTime, Point inAround, Resource inResource, int inCount)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.DropResource);
                m_Writer.Write((Int16)inAround.X);
                m_Writer.Write((Int16)inAround.Y);
                m_Writer.Write((Byte)inResource);
                m_Writer.Write((Byte)inCount);
                m_Writer.Flush();
            }
        }

        internal void AddMovable(Int64 inCycleTime, CyclePoint inPosition, MovableType inMovable)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.AddMovable);
                m_Writer.Write((Int64)inPosition.XCycles);
                m_Writer.Write((Int64)inPosition.YCycles);
                m_Writer.Write((Int16)inMovable);
                m_Writer.Flush();
            }
        }

        internal void ChangeToolSetting(Int64 inCycleTime, Resource inTool, int inNewTodo, double inNewPercentage)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.ChangeToolSetting);
                m_Writer.Write((Byte)inTool);
                m_Writer.Write((Int32)inNewTodo);
                m_Writer.Write((Double)inNewPercentage);
                m_Writer.Flush();
            }
        }

        internal void ChangeDistributionSetting(Int64 inCycleTime, String inBuildingClass, Resource inResource, bool inIsEnabled)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.ChangeDistributionSetting);
                m_Writer.Write((String)inBuildingClass);
                m_Writer.Write((Byte)inResource);
                m_Writer.Write((Boolean)inIsEnabled);
                m_Writer.Flush();
            }
        }

        internal void ChangeProfession(Int64 inCycleTime, SettlerProfessions inProfession, int inDelta)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.ChangeProfession);
                m_Writer.Write((Byte)inProfession);
                m_Writer.Write((SByte)inDelta);
                m_Writer.Flush();
            }
        }

        internal void AddMarketTransport(Int64 inCycleTime, long inBuildingUniqueID, Resource inResource)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.AddMarketTransport);
                m_Writer.Write((Int64)inBuildingUniqueID);
                m_Writer.Write((Byte)inResource);
                m_Writer.Flush();
            }
        }

        internal void RemoveMarketTransport(Int64 inCycleTime, long inBuildingUniqueID, Resource inResource)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.RemoveMarketTransport);
                m_Writer.Write((Int64)inBuildingUniqueID);
                m_Writer.Write((Byte)inResource);
                m_Writer.Flush();
            }
        }

        internal void SetTaskSuspended(Int64 inCycleTime, long inTaskID, bool isSuspended)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.SetTaskSuspended);
                m_Writer.Write((Int64)inTaskID);
                m_Writer.Write((Boolean)isSuspended);
                m_Writer.Flush();
            }
        }

        internal void RemoveBuildTask(Int64 inCycleTime, long inTaskID)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.RemoveBuildTask);
                m_Writer.Write((Int64)inTaskID);
                m_Writer.Flush();
            }
        }

        internal void RaiseTaskPriority(Int64 inCycleTime, long inTaskID)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.RaiseTaskPriority);
                m_Writer.Write((Int64)inTaskID);
                m_Writer.Flush();
            }
        }

        internal void RemoveBuilding(Int64 inCycleTime, long inBuildingID)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.RemoveBuilding);
                m_Writer.Write((Int64)inBuildingID);
                m_Writer.Flush();
            }
        }

        internal void RaiseBuildingPriority(Int64 inCycleTime, long inBuildingID)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.RaiseBuildingPriority);
                m_Writer.Write((Int64)inBuildingID);
                m_Writer.Flush();
            }
        }

        internal void LowerBuildingPriority(Int64 inCycleTime, long inBuildingID)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.LowerBuildingPriority);
                m_Writer.Write((Int64)inBuildingID);
                m_Writer.Flush();
            }
        }

        internal void SetBuildingSuspended(Int64 inCycleTime, long inBuildingID, bool isSuspended)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.SetBuildingSuspended);
                m_Writer.Write((Int64)inBuildingID);
                m_Writer.Write((Boolean)isSuspended);
                m_Writer.Flush();
            }
        }

        internal void SetWorkingArea(Int64 inCycleTime, long inBuildingID, Point inWorkingCenter)
        {
            lock (m_Lock)
            {
                m_Writer.Write((Int64)inCycleTime);
                m_Writer.Write((Byte)GameMapCommand.SetWorkingArea);
                m_Writer.Write((Int64)inBuildingID);
                m_Writer.Write((Int32)inWorkingCenter.X);
                m_Writer.Write((Int32)inWorkingCenter.Y);
                m_Writer.Flush();
            }
        }

        /// <summary>
        /// Read the next stored command, executes it for the given game map and returns
        /// the cycle time for the next command. This method should be used in a while loop
        /// until the return value is higher to the current cycle time...
        /// </summary>
        /// <param name="inTarget"></param>
        /// <returns></returns>
        internal void ReadNext(GameMap inTarget)
        {
            lock (m_Lock)
            {
                if (!m_IsReadMode)
                    throw new InvalidOperationException("Map file is not in read mode!");

                if (m_Stream.Length == m_Stream.Position)
                    NextCycle = -1;

                if (NextCycle == -1)
                    return;

                // read next command
                switch ((GameMapCommand)m_Reader.ReadByte())
                {
                    case GameMapCommand.AddBuilding:
                        {
                            int buildingID = m_Reader.ReadInt16();
                            int posX = m_Reader.ReadInt16();
                            int posY = m_Reader.ReadInt16();

                            inTarget.AddBuildingInternal(inTarget.ResolveBuildingTypeIndex(buildingID), new Point(posX, posY));
                        } break;

                    case GameMapCommand.DropResource:
                        {
                            int posX = m_Reader.ReadInt16();
                            int posY = m_Reader.ReadInt16();
                            Resource res = (Resource)m_Reader.ReadByte();
                            int count = m_Reader.ReadByte();

                            inTarget.DropResourceInternal(new Point(posX, posY), res, count);
                        } break;

                    case GameMapCommand.AddMovable:
                        {
                            Int64 xCycles = m_Reader.ReadInt64();
                            Int64 yCycles = m_Reader.ReadInt64();

                            inTarget.AddMovableInternal(new CyclePoint(xCycles, yCycles), (MovableType)m_Reader.ReadInt16());
                        } break;

                    case GameMapCommand.ChangeDistributionSetting:
                        {
                            String inBuildingClass = m_Reader.ReadString();
                            Resource inResource = (Resource)m_Reader.ReadByte();
                            bool inIsEnabled = m_Reader.ReadBoolean();

                            inTarget.ChangeDistributionSettingInternal(inBuildingClass, inResource, inIsEnabled);
                        } break;

                    case GameMapCommand.ChangeProfession:
                        {
                            SettlerProfessions inProfession = (SettlerProfessions)m_Reader.ReadByte();
                            int inDelta = m_Reader.ReadSByte();

                            inTarget.ChangeProfessionInternal(inProfession, inDelta);
                        } break;

                    case GameMapCommand.ChangeToolSetting:
                        {
                            Resource inTool = (Resource)m_Reader.ReadByte();
                            Int32 inNewTodo = m_Reader.ReadInt32();
                            Double inNewPercentage = m_Reader.ReadDouble();

                            inTarget.ChangeToolSettingInternal(inTool, inNewTodo, inNewPercentage);
                        } break;

                    case GameMapCommand.AddMarketTransport:
                        {
                            Int64 inBuildingUniqueID = m_Reader.ReadInt64();
                            Resource inResource = (Resource)m_Reader.ReadByte();

                            inTarget.AddMarketTransportInternal(inBuildingUniqueID, inResource);
                        } break;

                    case GameMapCommand.RemoveMarketTransport:
                        {
                            Int64 inBuildingUniqueID = m_Reader.ReadInt64();
                            Resource inResource = (Resource)m_Reader.ReadByte();

                            inTarget.RemoveMarketTransportInternal(inBuildingUniqueID, inResource);
                        } break;

                    case GameMapCommand.RaiseBuildingPriority:
                        {
                            Int64 inBuildingUniqueID = m_Reader.ReadInt64();

                            inTarget.RaiseBuildingPriorityInternal(inBuildingUniqueID);
                        } break;

                    case GameMapCommand.LowerBuildingPriority:
                        {
                            Int64 inBuildingUniqueID = m_Reader.ReadInt64();

                            inTarget.LowerBuildingPriorityInternal(inBuildingUniqueID);
                        } break;

                    case GameMapCommand.RemoveBuilding:
                        {
                            Int64 inBuildingUniqueID = m_Reader.ReadInt64();

                            inTarget.RemoveBuildingInternal(inBuildingUniqueID);
                        } break;

                    case GameMapCommand.SetBuildingSuspended:
                        {
                            Int64 inBuildingUniqueID = m_Reader.ReadInt64();
                            Boolean isSuspended = m_Reader.ReadBoolean();

                            inTarget.SetBuildingSuspendedInternal(inBuildingUniqueID, isSuspended);
                        } break;

                    case GameMapCommand.RaiseTaskPriority:
                        {
                            Int64 inTaskUniqueID = m_Reader.ReadInt64();

                            inTarget.RaiseTaskPriorityInternal(inTaskUniqueID);
                        } break;

                    case GameMapCommand.RemoveBuildTask:
                        {
                            Int64 inTaskUniqueID = m_Reader.ReadInt64();

                            inTarget.RemoveBuildTaskInternal(inTaskUniqueID);
                        } break;

                    case GameMapCommand.SetTaskSuspended:
                        {
                            Int64 inTaskUniqueID = m_Reader.ReadInt64();
                            Boolean isSuspended = m_Reader.ReadBoolean();

                            inTarget.SetTaskSuspendedInternal(inTaskUniqueID, isSuspended);
                        } break;

                    case GameMapCommand.SetWorkingArea:
                        {
                            Int64 inBuildingID = m_Reader.ReadInt64();
                            Int32 posX = m_Reader.ReadInt32();
                            Int32 posY = m_Reader.ReadInt32();

                            inTarget.SetWorkingAreaInternal(inBuildingID, new Point(posX, posY));
                        } break;

                    case GameMapCommand.Idle:
                        {
                            // just for time synchronization...
                        }break;

                    default:
                        throw new ArgumentException("Given game file \"" + FileName + "\" contains unknown commands.");
                }

                if (m_Stream.Length == m_Stream.Position)
                {
                    NextCycle = -1;

                    return;
                }

                long nextTime = m_Reader.ReadInt64();

                if (nextTime < NextCycle)
                    throw new InvalidDataException();

                NextCycle = nextTime;
            }
        }

        internal void Clear()
        {
            lock (m_Lock)
            {
                m_Stream.SetLength(0);
                m_Stream.Flush();
            }
        }

        internal void Fork(Int64 inCycleTime, String inFileName)
        {
            lock (m_Lock)
            {
                m_Stream.Flush();

                File.Delete(inFileName);
                File.Copy(FileName, inFileName);
            }


            using (var stream = File.OpenWrite(inFileName))
            {
                stream.Position = stream.Length;

                BinaryWriter writer = new BinaryWriter(stream);

                writer.Write((Int64)inCycleTime);
                writer.Write((Byte)GameMapCommand.Idle);
                writer.Flush();
            }
        }
    }
}
